//#preprocessor

/*
 * File: StandardDecrypter.java
 * Version: 1.0
 * Initial Creation: May 6, 2010 10:40:50 PM
 *
 * Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St,
 * Fortitude Valley, Queensland, Australia
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package com.sun.pdfview.decrypt;

import com.sun.pdfview.PDFObject;
import com.sun.pdfview.PDFParseException;
import com.sun.pdfview.PDFStringUtil;
import com.sun.pdfview.helper.PDFUtil;

import java.io.IOException;
//#ifndef BlackBerrySDK4.5.0 | BlackBerrySDK4.6.0 | BlackBerrySDK4.6.1 | BlackBerrySDK4.7.0 | BlackBerrySDK4.7.1
import java.nio.ByteBuffer;
//#else
import com.sun.pdfview.helper.nio.ByteBuffer;
//#endif
import java.util.Vector;

import net.rim.device.api.crypto.AESKey;
import net.rim.device.api.crypto.ARC4Key;
import net.rim.device.api.crypto.BlockDecryptorEngine;
import net.rim.device.api.crypto.BlockEncryptorEngine;
import net.rim.device.api.crypto.CryptoException;
import net.rim.device.api.crypto.CryptoTokenException;
import net.rim.device.api.crypto.CryptoUnsupportedOperationException;
import net.rim.device.api.crypto.DecryptorFactory;
import net.rim.device.api.crypto.Digest;
import net.rim.device.api.crypto.DigestFactory;
import net.rim.device.api.crypto.EncryptorFactory;
import net.rim.device.api.crypto.InitializationVector;
import net.rim.device.api.crypto.NoSuchAlgorithmException;
import net.rim.device.api.crypto.SymmetricKey;
import net.rim.device.api.util.Arrays;
import com.sun.pdfview.ResourceManager;
import com.sun.pdfview.i18n.ResourcesResource;

//TODO: Go through and redo exceptions, and make sure everything works properly

/**
 * Standard simple decrypter for versions 1, 2 and 4 of the Standard
 * password-based decryption mechanisms, as described in section 3.5 of
 * the PDF Reference version 1.7.
 *
 * @author Luke Kirby
 */
public class StandardDecrypter implements PDFDecrypter
{
	/**
     * Extra salt to add to AES-based decryption keys, as per PDF Reference 1.7
     */
    private static final byte[] AESV2_SALT = {'s', 'A', 'l', 'T'};
    
    /**
     * Describes an encryption algorithm to be used, declaring not only the
     * cipher type, but also key generation techniques
     */
    public static class EncryptionAlgorithm
    {
    	public static final int RC4 = 0;
    	public static final int AESV2 = RC4 + 1;
    	
    	public static boolean isRC4(int value)
    	{
    		return value == RC4;
    	}
    	
    	public static boolean isAES(int value)
    	{
    		return value == AESV2;
    	}
    }
    
    /**
     * Padding used to bring passwords up to 32 bytes, as specified by the
     * first step of Algorithm 3.2 in the PDF Reference version 1.7.
     */
    private final static byte[] PW_PADDING = new byte[]{
            0x28, (byte) 0xBF, 0x4E, 0x5E, 0x4E, 0x75, (byte)0x8A, 0x41,
            0x64, 0x00, 0x4E, 0x56, (byte)0xFF, (byte)0xFA, 0x01, 0x08,
            0x2E, 0x2E, 0x00, (byte)0xB6, (byte)0xD0, 0x68, 0x3E, (byte)0x80,
            0x2F, 0x0C, (byte)0xA9, (byte)0xFE, 0x64, 0x53, 0x69, 0x7A
    };
    
    /**
     * The specification of the RC4 cipher for JCE interactions
     */
    private static String CIPHER_RC4 = "RC4";
    /*
    /**
     * The key type for RC4 keys
     * /
    private static String KEY_RC4 = "RC4";
    */
    
    /**
     * The specification of the AES cipher for JCE interactions. As per the
     * spec, cipher-block chanining (CBC) mode and PKCS5 padding are used
     */
    private static String CIPHER_AES = "AES/CBC/PKCS5Padding";
    /*
    /**
     * The key type for AES keys
     * /
    private static String KEY_AES = "AES";
    */
    
    /**
     * Whether the owner password was specified
     */
    private boolean ownerAuthorised = false;
    
    /**
     * The general encryption key; may be mutated to form individual
     * stream/string encryption keys
     */
    private byte[] generalKeyBytes;
    
    /**
     * The encryption algorithm being employed
     */
    private int encryptionAlgorithm;
    
    /**
     * Class constructor
     *
     * @param encryptionAlgorithm the algorithm used for encryption
     * @param documentId the contents of the ID entry of the document's trailer
     * dictionary; can be null, but according to the spec, shouldn't be. Is
     * expected to be an array of two byte sequences.
     * @param keyBitLength the length of the key in bits; should be a multiple
     * of 8 between 40 and 128
     * @param revision the revision of the Standard encryption security handler
     * being employed. Should be 2, 3 or 4.
     * @param oValue the value of the O entry from the Encrypt dictionary
     * @param uValue the value of the U entry from the Encrypt dictionary
     * @param pValue the value of the P entry from the Encrypt dictionary
     * @param encryptMetadata whether metadata is being encrypted, as identified
     * by the Encrypt dict (with default true if not explicitly identified)
     * @param password the password; not null
     * @throws IOException if there's a problem reading the file
     * @throws EncryptionUnsupportedByPlatformException if the encryption is not
     * supported by the environment in which the code is executing
     * @throws EncryptionUnsupportedByProductException if PDFRenderer does not
     * currently support the specified encryption
     */
    public StandardDecrypter(int encryptionAlgorithm, PDFObject documentId, int keyBitLength, int revision, byte[] oValue, byte[] uValue, int pValue, 
    		boolean encryptMetadata, PDFPassword password) throws IOException, EncryptionUnsupportedByProductException, EncryptionUnsupportedByPlatformException
    {
        this.encryptionAlgorithm = encryptionAlgorithm;
        
        // The spec (sensibly) demands that the documentId be present,
        // but we'll play it safe
        final byte[] firstDocIdValue;
        if (documentId == null)
        {
            firstDocIdValue = null;
        }
        else
        {
            firstDocIdValue = documentId.getAt(0).getStream();
        }
        
        testJceAvailability(keyBitLength);
        
        try
        {
            final Vector passwordBytePossibilities = password.getPasswordBytes(false);
            for (int i = 0; generalKeyBytes == null && i < passwordBytePossibilities.size(); ++i)
            {
                final byte[] passwordBytes = (byte[])passwordBytePossibilities.elementAt(i);
                generalKeyBytes = checkOwnerPassword(passwordBytes, firstDocIdValue, keyBitLength, revision, oValue, uValue, pValue, encryptMetadata);
                if (generalKeyBytes != null)
                {
                    // looks like the password was the owner password!
                    ownerAuthorised = true;
                }
                else
                {
                    // try it as the user password
                    generalKeyBytes = checkUserPassword(passwordBytes, firstDocIdValue, keyBitLength, revision, oValue, uValue, pValue, encryptMetadata);
                }
            }
        }
        catch (CryptoException e)
        {
            // Unexpected, as our test of JCE availability should have caught
            // problems with cipher availability.
            // It may well be a problem with document content?
            throw new PDFParseException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getFormattedString(ResourcesResource.DECRYPT_STANDARD_CANT_CHECK_PASSWORD, new Object[]{e.getMessage()}));
        }

        if (generalKeyBytes == null)
        {
            throw new PDFAuthenticationFailureException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getString(ResourcesResource.DECRYPT_STANDARD_PASSWORD_FAIL_AUTHENT));
        }
    }
    
    public ByteBuffer decryptBuffer(String cryptFilterName, PDFObject streamObj, ByteBuffer streamBuf) throws PDFParseException
    {
        if (cryptFilterName != null)
        {
            throw new PDFParseException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getString(ResourcesResource.DECRYPT_STANDARD_ENCRYP_VER_UNSUPPORT_FILTER));
        }
        
        if (streamObj != null)
        {
            checkNums(streamObj.getObjNum(), streamObj.getObjGen());
        }
        
        final byte[] decryptionKeyBytes;
        if (streamObj == null)
        {
            // lack of a stream object indicates the unsalted key should be
            // used
            decryptionKeyBytes = getUnsaltedDecryptionKey();
        }
        else
        {
            decryptionKeyBytes = getObjectSaltedDecryptionKey(streamObj.getObjNum(), streamObj.getObjGen());
        }
        return decryptBuffer(streamBuf, decryptionKeyBytes);
    }
    
    public String decryptString(int objNum, int objGen, String inputBasicString) throws PDFParseException
    {
        final byte[] crypted = PDFStringUtil.asBytes(inputBasicString);
        final byte[] decryptionKey = getObjectSaltedDecryptionKey(objNum, objGen);
        final ByteBuffer decrypted = decryptBuffer(ByteBuffer.wrap(crypted), decryptionKey);
        return PDFStringUtil.asBasicString(decrypted.array(), decrypted.arrayOffset(), decrypted.limit());
    }
    
    public boolean isOwnerAuthorised()
    {
        return ownerAuthorised;
    }
    
    public boolean isEncryptionPresent()
    {
        return true;
    }
    
    public boolean isEncryptionPresent(String cryptFilterName)
    {
        return true;
    }
    
    /**
     * Test that the platform (i.e., the JCE) can offer us all of the ciphers at
     * the key length we need for content decryption. This shouldn't be a
     * problem on the Java 5 platform unless a particularly restrictive policy
     * file is in place. Calling this on construction should avoid problems like
     * these being exposed as PDFParseExceptions as they're used during
     * decryption and key establishment.
     *
     * @param keyBitLength the length of the content key, in bits
     * @throws EncryptionUnsupportedByPlatformException if the platform does not
     * support the required ciphers and key lengths
     * @throws PDFParseException if there's an internal error while testing
     * cipher availability
     */
    private void testJceAvailability(int keyBitLength) throws EncryptionUnsupportedByPlatformException, PDFParseException
    {
        // we need to supply a little buffer for AES, which will look
        // for an initialisation vector of 16 bytes
        final byte[] junkBuffer = new byte[16];
        Arrays.fill(junkBuffer, (byte)0xAE);
        // test using the longer key length for salted content so that
        // we can check for maximum key length problems
        final byte[] junkKey = new byte[getSaltedContentKeyByteLength(keyBitLength / 8)];
        Arrays.fill(junkKey, (byte)0xAE);
        
        try
        {
            createAndInitialiseContentCipher(ByteBuffer.wrap(junkBuffer), junkKey);
        }
        catch (PDFParseException e)
        {
            throw new PDFParseException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getFormattedString(ResourcesResource.DECRYPT_STANDARD_FAIL_MAKE_TEST_CIPHER, new Object[]{e.getMessage()}));
        }
        catch (NoSuchAlgorithmException e)
        {
            throw new EncryptionUnsupportedByPlatformException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getString(ResourcesResource.DECRYPT_STANDARD_BB_NO_OFFER_CIPHER));
        }
        /*
        catch (NoSuchPaddingException e)
        {
            throw new EncryptionUnsupportedByPlatformException("JCE does not offer required padding");
        }
        catch (InvalidKeyException e)
        {
            throw new EncryptionUnsupportedByPlatformException("JCE does accept key size of " + (getSaltedContentKeyByteLength() * 8) + " bits- could it be a policy restriction?");
        } 
        catch (InvalidAlgorithmParameterException e)
        {
            throw new EncryptionUnsupportedByPlatformException("JCE did not accept cipher parameter");
        }
        */
        catch (CryptoException e)
        {
        	throw new EncryptionUnsupportedByPlatformException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getString(ResourcesResource.DECRYPT_STANDARD_BB_NO_ACCEPT_CIPHER_PARAM));
		}
        
        try
        {
            createMD5Digest();
        }
        catch (NoSuchAlgorithmException e)
        {
            throw new EncryptionUnsupportedByPlatformException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getString(ResourcesResource.DECRYPT_STANDARD_NO_MD5));
        }
        
        if (encryptionAlgorithm != EncryptionAlgorithm.RC4)
        {
            // we still need RC4 for U and O value checks. Check again!
            final BlockDecryptorEngine rc4;
            // 40 byte key is used for base U and O ciphers
            final byte[] rc4JunkKey = new byte[5];
            Arrays.fill(junkKey, (byte)0xAE);
            try
            {
                rc4 = createRC4Cipher_Decryptor(createRC4Key(rc4JunkKey));
            }
            catch(CryptoTokenException te)
            {
            	throw new EncryptionUnsupportedByPlatformException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getString(ResourcesResource.DECRYPT_STANDARD_BB_RC4_KEY));
            }
            catch (CryptoException e)
            {
                throw new EncryptionUnsupportedByPlatformException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getString(ResourcesResource.DECRYPT_STANDARD_BB_NO_OFFER_RC4));
            }
        }
    }
    
    /**
     * Decrypt a buffer
     *
     * @param encrypted the encrypted content
     * @param decryptionKeyBytes the key to use for decryption
     * @return a freshly allocated buffer containing the decrypted content
     * @throws PDFParseException if there's a problem decrypting the content
     */
    private ByteBuffer decryptBuffer(ByteBuffer encrypted, byte[] decryptionKeyBytes) throws PDFParseException
    {
        final BlockDecryptorEngine cipher;
        try
        {
            cipher = createAndInitialiseContentCipher(encrypted, decryptionKeyBytes);
        }
        catch (CryptoException e)
        {
            // we should have caught this earlier in testCipherAvailability
            throw new PDFParseException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getFormattedString(ResourcesResource.DECRYPT_STANDARD_PLATFORM_CIPHER_DENIAL, new Object[]{e.getMessage()}));
        }
        
        try
        {
            // the decrypted content will never be more than the encrypted
            // content. Thanks to padding, this buffer will be at most 16
            // bytes bigger than the encrypted content
            final ByteBuffer decryptedBuf = ByteBuffer.allocateDirect(encrypted.remaining());
            decrypt(cipher, encrypted, decryptedBuf);
            decryptedBuf.flip();
            return decryptedBuf;
        }
        catch (CryptoException e)
        {
            throw new PDFParseException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getFormattedString(ResourcesResource.DECRYPT_STANDARD_DECRYPT_FAIL, new Object[]{e.getMessage()}));
        }
    }
    
    private static void decrypt(BlockDecryptorEngine cipher, ByteBuffer source, ByteBuffer dest) throws CryptoException
    {
    	int len = source.remaining();
    	byte[] dat = new byte[len];
    	source.get(dat);
    	cipher.decrypt(dat, 0, dat, 0);
    	dest.put(dat);
    }
    
    /**
     * Setup the cipher for decryption
     *
     * @param encrypted the encrypted content; required by AES encryption so
     * that the initialisation vector can be established
     * @param decryptionKeyBytes the bytes for the decryption key
     * @return a content decryption cypher, ready to accept input
     * @throws PDFParseException if the encrypted buffer is malformed or on an
     * internal error
     * @throws NoSuchAlgorithmException if the cipher algorithm is not supported
     * by the platform
     * @throws NoSuchPaddingException if the cipher padding is not supported by
     * the platform
     * @throws InvalidKeyException if the key is invalid according to the
     * cipher, or too long
     * @throws InvalidAlgorithmParameterException if the cipher parameters are
     * bad
     */
    private BlockDecryptorEngine createAndInitialiseContentCipher(ByteBuffer encrypted, byte[] decryptionKeyBytes) throws PDFParseException, NoSuchAlgorithmException, CryptoException
    {
        final BlockDecryptorEngine cipher;
        if (EncryptionAlgorithm.isRC4(encryptionAlgorithm))
        {
        	final SymmetricKey rc4Key = createRC4Key(decryptionKeyBytes);
            cipher = DecryptorFactory.getBlockDecryptorEngine(rc4Key, CIPHER_RC4);
        }
        else if (EncryptionAlgorithm.isAES(encryptionAlgorithm))
        {
            final byte[] initialisationVector = new byte[16];
            if (encrypted.remaining() >= initialisationVector.length)
            {
                encrypted.get(initialisationVector);
            }
            else
            {
                throw new PDFParseException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getString(ResourcesResource.DECRYPT_STANDARD_INIT_VECTOR_TOO_SMALL));
            }
            
            final AESKey aesKey = new AESKey(decryptionKeyBytes);
            final InitializationVector aesIv = new InitializationVector(initialisationVector);
            cipher = DecryptorFactory.getBlockDecryptorEngine(aesKey, CIPHER_AES, aesIv);
        }
        else
        {
            throw new PDFParseException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getFormattedString(ResourcesResource.DECRYPT_STANDARD_UNHANDLED_CIPHER_TYPE, new Object[]{new Integer(encryptionAlgorithm)}));
        }
        return cipher;
    }
    
    /**
     * Get the unsalted content decryption key, used for streams with specific
     * crypt filters, which aren't specific to particular objects
     *
     * @return the general key
     */
    private byte[] getUnsaltedDecryptionKey()
    {
        return generalKeyBytes;
    }
    
    /**
     * Get a decryption key salted with an object number and object generation,
     * for use when decrypting a string or stream within an object numbered so
     *
     * @param objNum the object number
     * @param objGen the object generation
     * @return the key to be used for decrypting data associated with the object
     *         numbered so
     * @throws PDFParseException if the MD5 digest is not available
     */
    private byte[] getObjectSaltedDecryptionKey(int objNum, int objGen) throws PDFParseException
    {
        byte[] decryptionKeyBytes;
        final Digest md5;
        try
        {
            md5 = createMD5Digest();
        }
        catch (NoSuchAlgorithmException e)
        {
            // unexpected, as we will already have tested availability
            throw new PDFParseException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getString(ResourcesResource.DECRYPT_STANDARD_NO_MD5_DIGEST));
        }
        md5.update(this.generalKeyBytes);
        md5.update((byte)objNum);
        md5.update((byte)(objNum >> 8));
        md5.update((byte)(objNum >> 16));
        md5.update((byte)objGen);
        md5.update((byte)(objGen >> 8));
        if (encryptionAlgorithm == EncryptionAlgorithm.AESV2)
        {
            md5.update(AESV2_SALT);
        }
        final byte[] hash = md5.getDigest();
        final int keyLen = getSaltedContentKeyByteLength();
        decryptionKeyBytes = new byte[keyLen];
        System.arraycopy(hash, 0, decryptionKeyBytes, 0, keyLen);
        return decryptionKeyBytes;
    }
    
    /**
     * Get the length of a salted key
     *
     * @return length in bytes
     */
    private int getSaltedContentKeyByteLength()
    {
        return getSaltedContentKeyByteLength(generalKeyBytes.length);
    }
    
    /**
     * Get the length of salted keys, in bytes. Unsalted keys will be the same
     * length as {@link #generalKeyBytes}
     *
     * @param generalKeyByteLength the length of the general key, in bytes
     * @return byte length of salted keys
     */
    private int getSaltedContentKeyByteLength(int generalKeyByteLength)
    {
        return Math.min(generalKeyByteLength + 5, 16);
    }
    
    /**
     * Check that object number and object generations are well-formed. It is
     * possible for some {@link PDFObject}s to have uninitialised object numbers
     * and generations, but such objects should not required decryption
     *
     * @param objNum the object number
     * @param objGen the object generation
     * @throws PDFParseException if the object numbering indicates that they
     * aren't true object numbers
     */
    private void checkNums(int objNum, int objGen) throws PDFParseException
    {
        if (objNum < 0)
        {
            throw new PDFParseException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getString(ResourcesResource.DECRYPT_STANDARD_BOGUS_NUMBER));
        }
        else if (objGen < 0)
        {
            throw new PDFParseException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getString(ResourcesResource.DECRYPT_STANDARD_BOGUS_GENERATION));
        }
    }
    
    /**
     * Calculate what the U value should consist of given a particular key and
     * document configuration. Correponds to Algorithms 3.4 and 3.5 of the
     * PDF Reference version 1.7
     *
     * @param generalKey the general encryption key
     * @param firstDocIdValue the value of the first element in the document's
     * ID entry in the trailer dictionary
     * @param revision the revision of the security handler
     * @return the U value for the given configuration
     * @throws CryptoException if there's an error getting required
     * ciphers, etc. (unexpected, since a check for algorithm availability is
     * performed on construction)
     * @throws EncryptionUnsupportedByProductException if the revision is not
     * supported
     */
    private byte[] calculateUValue(byte[] generalKey, byte[] firstDocIdValue, int revision) throws CryptoException, EncryptionUnsupportedByProductException
    {
        if (revision == 2)
        {
            // Algorithm 3.4: Computing the encryption dictionaries U (user
            // password) value (Revision 2)
        	
            // Step 1 is provided to us as the parameter generalKey:
            //  Create an encryption key based on the user password string, as
            //  described in Algorithm 3.2
        	
            // Step 2: Encrypt the 32-byte padding string shown in step 1 of
            // Algorithm 3.2, using an RC4 encryption function with the
            // encryption key from the preceding step.
        	
            BlockEncryptorEngine rc4 = createRC4Cipher_Encryptor(createRC4Key(generalKey));
            return crypt(rc4, PW_PADDING);
        }
        else if (revision >= 3)
        {
            // Algorithm 3.5: Computing the encryption dictionaries U (user
            // password) value (Revision 3 or greater)
        	
            // Step 1 is provided to us as the parameter generalKey:
            //  Create an encryption key based on the user password string, as
            //  described in Algorithm 3.2
        	
            // Step 2: Initialize the MD5 hash function and pass the 32-byte
            // padding string shown in step 1 of Algorithm 3.2 as input to this
            // function
            Digest md5 = createMD5Digest();
            md5.update(PW_PADDING);
            
            // Step 3: Pass the first element of the files file identifier
            // array (the value of the ID entry in the documents trailer
            // dictionary; see Table 3.13 on page 97) to the hash function and
            // finish the hash. (See implementation note 26 in Appendix H.)
            if (firstDocIdValue != null)
            {
                md5.update(firstDocIdValue);
            }
            final byte[] hash = md5.getDigest();
            
            // Step 4: Encrypt the 16-byte result of the hash, using an RC4
            // encryption function with the encryption key from step 1.
            BlockEncryptorEngine rc4 = createRC4Cipher_Encryptor(createRC4Key(generalKey));
            final byte[] v = crypt(rc4, hash);
            
            // Step 5: Do the following 19 times: Take the output from the
            // previous invocation of the RC4 function and pass it as input to
            // a new invocation of the function; use an encryption key generated
            // by taking each byte of the original encryption key (obtained in
            // step 1) and performing an XOR (exclusive or) operation between
            // that byte and the single-byte value of the iteration counter
            // (from 1 to 19).
            rc4shuffle(v, generalKey, rc4);
            
            // Step 6: Append 16 bytes of arbitrary padding to the output from
            // the final invocation of the RC4 function and store the 32-byte
            // result as the value of the U entry in the encryption dictionary.
            PDFUtil.assert(v.length == 16, "v.length == 16");
            final byte[] entryValue = new byte[32];
            System.arraycopy(v, 0, entryValue, 0, v.length);
            System.arraycopy(v, 0, entryValue, 16, v.length);
            return entryValue;
        }
        else
        {
            throw new EncryptionUnsupportedByProductException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getFormattedString(ResourcesResource.DECRYPT_STANDARD_UNSUPPORTED_SECURITY_HANDLER_REV, new Object[]{new Integer(revision)}));
        }
    }
    
    /**
     * Calculate what the O value of the Encrypt dict should look like given a
     * particular configuration. Not used, but useful for reference; this
     * process is reversed to determine whether a given password is the
     * owner password. Corresponds to Algorithm 3.3 of the PDF Reference
     * version 1.7.
     *
     * @see #checkOwnerPassword
     * @param ownerPassword the owner password
     * @param userPassword the user password
     * @param keyBitLength the key length in bits (40-128)
     * @param revision the security handler revision
     * @return the O value entry
     * @throws CryptoException if ciphers are unavailable or
     *  inappropriately used
     */
    private byte[] calculuateOValue(byte[] ownerPassword, byte[] userPassword, int keyBitLength, int revision) throws CryptoException
    {
        // Steps 1-4
        final byte[] rc4KeyBytes = getInitialOwnerPasswordKeyBytes(ownerPassword, keyBitLength, revision);
        final BlockEncryptorEngine rc4 = createRC4Cipher_Encryptor(createRC4Key(rc4KeyBytes));
        
        // Step 5: Pad or truncate the user password string as described in step
        // 1 of Algorithm 3.2.
        // Step 6: Encrypt the result of step 5, using an RC4 encryption
        // function with the encryption key obtained in step 4.
        byte[] pwvalue = crypt(rc4, padPassword(userPassword));
        
        // Step 7: (Revision 3 or greater) Do the following 19 times: Take the
        // output from the previous invocation of the RC4 function and pass it
        // as input to a new invocation of the function; use an encryption key
        // generated by taking each byte of the encryption key obtained in step
        // 4 and performing an XOR (exclusive or) operation between
        if (revision >= 3)
        {
            rc4shuffle(pwvalue, rc4KeyBytes, rc4);
        }
        PDFUtil.assert(pwvalue.length == 32, "pwvalue.length == 32");
        return pwvalue;
    }
    
    /**
     * Check to see whether a given password is the owner password. Corresponds
     * to algorithm 3.6 of PDF Reference version 1.7.
     *
     * @param ownerPassword the suggested owner password (may be null or
     * empty)
     * @param firstDocIdValue the byte stream from the first element of the
     *  value of the ID entry in the trailer dictionary
     * @param keyBitLength the key length in bits
     * @param revision the security handler revision
     * @param oValue the O value from the Encrypt dictionary
     * @param uValue the U value from the Encrypt dictionary
     * @param pValue the P value from the Encrypt dictionary
     * @param encryptMetadata the EncryptMetadata entry from the Encrypt dictionary
     *  (or false if not present or revision &lt;= 3)
     * @return the general/user key bytes if the owner password is currect,
     *  <code>null</code> otherwise
     * @throws CryptoException if there's a problem with
     * cipher or digest usage; unexpected
     * @throws EncryptionUnsupportedByProductException if PDFRenderer doesn't
     * support the security handler revision
     * @throws PDFParseException if the document is malformed
     */
    private byte[] checkOwnerPassword(byte[] ownerPassword, byte[] firstDocIdValue, int keyBitLength, int revision, byte[] oValue, byte[] uValue, int pValue, 
    		boolean encryptMetadata) throws CryptoException, EncryptionUnsupportedByProductException, PDFParseException
    {
        // Step 1: Compute an encryption key from the supplied password string,
        // as described in steps 1 to 4 of Algorithm 3.3.
        final byte[] rc4KeyBytes = getInitialOwnerPasswordKeyBytes(ownerPassword, keyBitLength, revision);
        final BlockDecryptorEngine rc4 = createRC4Cipher_Decryptor(createRC4Key(rc4KeyBytes));
        
        // Step 2:
        final byte[] possibleUserPassword;
        if (revision == 2)
        {
            // (Revision 2 only) Decrypt the value of the encryption
            // dictionaries O entry, using an RC4 encryption function with the
            // encryption key computed in step 1.
        	
            possibleUserPassword = crypt(rc4, oValue);
        }
        else if (revision >= 3) 
        {
            // (Revision 3 or greater) Do the following 20 times: Decrypt the
            // value of the encryption dictionaries O entry (first iteration) or
            // the output from the previous iteration (all subsequent
            // iterations), using an RC4 encryption function with a different
            // encryption key at each iteration. The key is generated by taking
            // the original key (obtained in step 1) and performing an XOR
            // (exclusive or) operation between each byte of the key and the
            // single-byte value of the iteration counter (from 19 to 0).
        	
            // unshuffle the O entry; the unshuffle operation also
            // contains the final decryption with the original key
            possibleUserPassword = new byte[32];
            System.arraycopy(oValue, 0, possibleUserPassword, 0, possibleUserPassword.length);
            rc4unshuffle(rc4, possibleUserPassword, rc4KeyBytes);
        }
        else
        {
            throw new EncryptionUnsupportedByProductException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getFormattedString(ResourcesResource.DECRYPT_STANDARD_UNSUPPORTED_REV, new Object[]{new Integer(revision)}));
        }
        
        // Step 3: The result of step 2 purports to be the user password.
        // Authenticate this user password using Algorithm 3.6. If it is
        // correct, the password supplied is the correct owner password.
        return checkUserPassword(possibleUserPassword, firstDocIdValue, keyBitLength, revision, oValue, uValue, pValue, encryptMetadata);
    }
    
    /**
     * Establish the key to be used for the generation and validation
     * of the user password via the O entry. Corresponds to steps 1-4 in
     * Algorithm 3.3 of the PDF Reference version 1.7.
     * @param ownerPassword the owner password
     * @param keyBitLength the length of the key in bits
     * @param revision the security handler revision
     * @return the key bytes to use for generation/validation of the O entry
     * @throws CryptoException if there's a problem wranling ciphers
     */
    private byte[] getInitialOwnerPasswordKeyBytes( byte[] ownerPassword, int keyBitLength, int revision) throws CryptoException
    {
        final Digest md5 = createMD5Digest();
        
        // Step 1: Pad or truncate the owner password string as described in
        // step 1 of Algorithm 3.2. If there is no owner password, use the user
        // password instead. (See implementation note 27 in Appendix H.)
        // Step 2: Initialize the MD5 hash function and pass the result of step 1 as
        // input to this function.
        md5.update(padPassword(ownerPassword));
        
        // Step 3.(Revision 3 or greater) Do the following 50 times: Take the
        // output from the previous MD5 hash and pass it as input into a new MD5
        // hash
        final byte[] hash = md5.getDigest();
        if (revision >= 3)
        {
            for (int i = 0; i < 50; ++i)
            {
                md5.update(hash);
                digestTo(md5, hash);
            }
        }
        
        // Step 4: Create an RC4 encryption key using the first n bytes of
        // the output from the final MD5 hash, where n is always 5 for revision
        // 2 but, for revision 3 or greater, depends on the value of the
        // encryption dictionary Length entry
        final byte[] rc4KeyBytes = new byte[keyBitLength / 8];
        System.arraycopy(hash, 0, rc4KeyBytes, 0, rc4KeyBytes.length);
        return rc4KeyBytes;
    }

    /**
     * Check to see whether a provided user password is correct with respect
     * to an Encrypt dict configuration. Corresponds to algorithm 3.6 of
     * the PDF Reference version 1.7
     * @param userPassword the user password to test; may be null or empty
     * @param firstDocIdValue the byte stream from the first element of the
     *  value of the ID entry in the trailer dictionary
     * @param keyBitLength the length of the key in bits
     * @param revision the security handler revision
     * @param oValue the O value from the Encrypt dictionary
     * @param uValue the U value from the Encrypt dictionary
     * @param pValue the P value from the Encrypt dictionary
     * @param encryptMetadata the EncryptMetadata entry from the Encrypt dictionary
     *  (or false if not present or revision &lt;= 3)
     * @return the general/user encryption key if the user password is correct,
     *  or null if incorrect
     * @throws CryptoException if there's a problem with
     * cipher or digest usage; unexpected
     * @throws EncryptionUnsupportedByProductException if PDFRenderer doesn't
     * support the security handler revision
     * @throws PDFParseException if the document is improperly constructed
     */
    private byte[] checkUserPassword(byte[] userPassword, byte[] firstDocIdValue, int keyBitLength, int revision, byte[] oValue, byte[] uValue, int pValue, 
    		boolean encryptMetadata) throws CryptoException,  EncryptionUnsupportedByProductException, PDFParseException
    {
        // Algorithm 3.6: Authenticating the user password
    	
        // Step 1: Perform all but the last step of Algorithm 3.4 (Revision 2)
        // or Algorithm 3.5 (Revision 3 or greater) using the supplied password
        // string
        //
        // I.e., figure out what the general key would be with the
        // given password
        // Algorithm 3.4/5,Step1:
        // Determine general key based on user password, as per Algorithm 3.2
        final byte[] generalKey = calculateGeneralEncryptionKey(userPassword, firstDocIdValue, keyBitLength, revision, oValue, pValue, encryptMetadata);
        // Algorithm 3.4/5,RemainingSteps:
        final byte[] calculatedUValue = calculateUValue(generalKey, firstDocIdValue, revision);
        
        // Step 2: If the result of step 1 is equal to the value of the
        // encryption dictionaries U entry (comparing on the first 16 bytes in
        // the case of Revision 3 or greater), the password supplied is the
        // correct user password. The key obtained in step 1 (that is, in the
        // first step of Algorithm 3.4 or 3.5) can be used to decrypt the
        // document using Algorithm 3.1 on page 119.
        PDFUtil.assert(calculatedUValue.length == 32, "calculatedUValue.length == 32");
        if (uValue.length != calculatedUValue.length)
        {
            throw new PDFParseException(ResourceManager.getResource(ResourceManager.LOCALIZATION).getFormattedString(ResourcesResource.DECRYPT_STANDARD_IMPROPER_U_LEN, new Object[]{new Integer(uValue.length)}));
        }
        // Only the first 16 bytes are significant if using revision > 2
        final int numSignificantBytes = revision == 2 ? 32 : 16;
        for (int i = 0; i < numSignificantBytes; ++i)
        {
            if (uValue[i] != calculatedUValue[i])
            {
                return null;
            }
        }
        return generalKey;
    }
    
    
    /**
     * Determine what the general encryption key is, given a configuration. This
     * corresponds to Algorithm 3.2 of PDF Reference version 1.7.
     *
     * @param userPassword the desired user password; may be null or empty
     * @param firstDocIdValue the byte stream from the first element of the
     * value of the ID entry in the trailer dictionary
     * @param keyBitLength the length of the key in bits
     * @param revision the security handler revision
     * @param oValue the O value from the Encrypt dictionary
     * @param pValue the P value from the Encrypt dictionary
     * @param encryptMetadata the EncryptMetadata entry from the Encrypt
     * dictionary (or false if not present or revision &lt;= 3)
     * @return the general encryption key
     * @throws CryptoException if an error occurs when obtaining
     *  and operating ciphers/digests
     */
    private byte[] calculateGeneralEncryptionKey(byte[] userPassword, byte[] firstDocIdValue, int keyBitLength, int revision, byte[] oValue, int pValue, 
    		boolean encryptMetadata) throws CryptoException
    {
        // Algorithm 3.2: Computing an encryption key
    	
        // Step 1: Pad or truncate the password string to exactly 32 bytes...
        final byte[] paddedPassword = padPassword(userPassword);
        
        // Step 2: Initialize the MD5 hash function and pass the result of step
        // 1 as input to this function.
        Digest md5 = createMD5Digest();
        md5.reset();
        md5.update(paddedPassword);
        
        // Step 3: Pass the value of the encryption dictionaries O entry to the
        // MD5 hash function. (Algorithm 3.3 shows how the O value is computed.)
        md5.update(oValue);
        
        // Step 4: Treat the value of the P entry as an unsigned 4-byte integer
        // and pass these bytes to the MD5 hash function, low-order byte first
        md5.update((byte)(pValue & 0xFF));
        md5.update((byte)((pValue >> 8) & 0xFF));
        md5.update((byte)((pValue >> 16) & 0xFF));
        md5.update((byte)(pValue >> 24));
        
        // Step 5: Pass the first element of the file's file identifier array
        // (the value of the ID entry in the documents trailer dictionary; see
        // Table 3.13 on page 97) to the MD5 hash function. (See implementation
        // note 26 in Appendix H.)
        if (firstDocIdValue != null)
        {
            md5.update(firstDocIdValue);
        }
        
        // Step 6: (Revision 4 or greater) If document metadata is not being
        // encrypted, pass 4 bytes with the value 0xFFFFFFFF to the MD5 hash
        // function
        if (revision >= 4 && !encryptMetadata)
        {
            for (int i = 0; i < 4; ++i)
            {
                md5.update((byte)0xFF);
            }
        }
        
        // Step 7: finish the hash
        byte[] hash = md5.getDigest();
        
        final int keyLen = revision == 2 ? 5 : (keyBitLength / 8);
        final byte[] key = new byte[keyLen];
        
        // Step 8: (Revision 3 or greater) Do the following 50 times: Take the
        // output from the previous MD5 hash and pass the first n bytes of the
        // output as input into a new MD5 hash, where n is the number of bytes
        // of the encryption key as defined by the value of the encryption
        // dictionaries Length entry
        if (revision >= 3)
        {
            for (int i = 0; i < 50; ++i)
            {
                md5.update(hash, 0, key.length);
                digestTo(md5, hash);
            }
        }
        
        // Set the encryption key to the first n bytes of the output from the
        // final MD5 hash, where n is always 5 for revision 2 but, for revision
        // 3 or greater, depends on the value of the encryption dictionaries
        // Length entry.
        System.arraycopy(hash, 0, key, 0, key.length);
        return key;
    }
    
    /**
     * Pad a password as per step 1 of Algorithm 3.2 of the PDF Reference
     * version 1.7
     * @param password the password, may be null or empty
     * @return the padded password, always 32 bytes long
     */
    private byte[] padPassword(byte[] password)
    {
        if (password == null)
        {
            password = new byte[0];
        }
        
        // Step 1: Pad or truncate the password string to exactly 32 bytes. If
        // the password string is more than 32 bytes long, use only its first 32
        // bytes; if it is less than 32 bytes long, pad it by appending the
        // required number of additional bytes from the beginning of the
        // following padding string:
        // < 28 BF 4E 5E 4E 75 8A 41 64 00 4E 56 FF FA 01 08
        //   2E 2E 00 B6 D0 68 3E 80 2F 0C A9 FE 64 53 69 7A >
        // That is, if the password string is n bytes long, append the first 32
        // n bytes of the padding string to the end of the password string. If
        // the password string is empty (zero-length), meaning there is no user
        // password, substitute the entire padding string in its place.
        
        byte[] padded = new byte[32];
        // limit password to 32 bytes
        final int numContributingPasswordBytes =  password.length > padded.length ? padded.length : password.length;
        System.arraycopy(password, 0, padded, 0, numContributingPasswordBytes);
        // Copy padding
        if (password.length < padded.length)
        {
            System.arraycopy(PW_PADDING, 0, padded, password.length, padded.length - password.length);
        }
        return padded;
    }
    
    /**
     * Encrypt some bytes
     *
     * @param cipher the cipher
     * @param input the plaintext
     * @return the crypt text
     */
    private byte[] crypt(BlockDecryptorEngine cipher, byte[] input) throws CryptoTokenException
    {
    	byte[] temp = new byte[input.length + 1024];
    	cipher.decrypt(input, 0, temp, 0);
    	byte[] results = new byte[cipher.getBlockLength()];
    	System.arraycopy(temp, 0, results, 0, results.length);
    	return results;
    }
    
    /**
     * Encrypt some bytes
     *
     * @param cipher the cipher
     * @param input the plaintext
     * @return the crypt text
     */
    private byte[] crypt(BlockEncryptorEngine cipher, byte[] input) throws CryptoTokenException
    {
    	byte[] temp = new byte[input.length + 1024];
    	cipher.encrypt(input, 0, temp, 0);
    	byte[] results = new byte[cipher.getBlockLength()];
    	System.arraycopy(temp, 0, results, 0, results.length);
    	return results;
    }
    
    /**
     * Shuffle some input using a series of RC4 encryptions with slight
     * mutations of an given key per iteration. Shuffling happens in place.
     * Refer to the documentation of the algorithm steps where this is called.
     *
     * @param shuffle the bytes to be shuffled
     * @param key the original key
     * @param rc4 the cipher to use
     * @throws CryptoException if there's a problem with cipher
     *  operation
     */
    private void rc4shuffle(byte[] shuffle, byte[] key, BlockEncryptorEngine rc4) throws CryptoException
    {
        final byte[] shuffleKey = new byte[key.length];
        for (int i = 1; i <= 19; ++i)
        {
            for (int j = 0; j < shuffleKey.length; ++j)
            {
                shuffleKey[j] = (byte) (key[j] ^ i);
            }
            BlockEncryptorEngine rc4n = EncryptorFactory.getBlockEncryptorEngine(createRC4Key(shuffleKey), rc4.getAlgorithm());
            cryptInPlace(rc4n, shuffle);
        }
    }
    
    /**
     * Reverse the {@link #rc4shuffle} operation, and the operation
     * that invariable preceeds it, thereby obtaining an original message
     * @param rc4 the RC4 cipher to use
     * @param shuffle the bytes in which shuffling will take place; unshuffling
     *  happens in place
     * @param key the encryption key
     * @throws CryptoException if there's a problem with cipher
     *  operation
     */
    private void rc4unshuffle(BlockDecryptorEngine rc4, byte[] shuffle, byte[] key) throws CryptoException
    {
        // there's an extra unshuffle at the end with the original key -
        // this is why we end with i == 0, where the shuffle key will be the key
        final byte[] shuffleKeyBytes = new byte[key.length];
        for (int i = 19; i >= 0; --i)
        {
            for (int j = 0; j < shuffleKeyBytes.length; ++j)
            {
                shuffleKeyBytes[j] = (byte) (key[j] ^ i);
            }
            BlockDecryptorEngine rc4n = DecryptorFactory.getBlockDecryptorEngine(createRC4Key(shuffleKeyBytes), rc4.getAlgorithm());
            cryptInPlace(rc4n, shuffle);
        }
    }
    
    /**
     * Encrypt something in place
     * @param rc4 the cipher to use; must be a stream cipher producing
     *  identical output length to input (e.g., RC4)
     * @param buffer the buffer to read input from and write output to
     */
    private void cryptInPlace(BlockEncryptorEngine rc4, byte[] buffer) throws CryptoTokenException
    {
    	rc4.encrypt(buffer, 0, buffer, 0);
    }
    
    /**
     * Decrypt something in place
     * @param rc4 the cipher to use; must be a stream cipher producing
     *  identical output length to input (e.g., RC4)
     * @param buffer the buffer to read input from and write output to
     */
    private void cryptInPlace(BlockDecryptorEngine rc4, byte[] buffer) throws CryptoTokenException
    {
    	rc4.decrypt(buffer, 0, buffer, 0);
    }
    
    /**
     * Create a new RC4 cipher. Should always be available for supported
     * platforms.
     * @return the cipher
     * @throws NoSuchAlgorithmException if the RC4 cipher is unavailable
     */
    private BlockDecryptorEngine createRC4Cipher_Decryptor(SymmetricKey key) throws NoSuchAlgorithmException, CryptoTokenException, CryptoUnsupportedOperationException, CryptoException
    {
    	return DecryptorFactory.getBlockDecryptorEngine(key, CIPHER_RC4);
    }
    
    /**
     * Create a new RC4 cipher. Should always be available for supported
     * platforms.
     * @return the cipher
     * @throws NoSuchAlgorithmException if the RC4 cipher is unavailable
     */
    private BlockEncryptorEngine createRC4Cipher_Encryptor(SymmetricKey key) throws NoSuchAlgorithmException, CryptoTokenException, CryptoUnsupportedOperationException, CryptoException
    {
    	return EncryptorFactory.getBlockEncryptorEngine(key, CIPHER_RC4);
    }
    
    /**
     * Create an MD5 digest. Should always be available for supported
     * platforms.
     * @return the MD5 digest
     * @throws NoSuchAlgorithmException if the digest is not available
     */
    private Digest createMD5Digest() throws NoSuchAlgorithmException
    {
        return DigestFactory.getInstance("MD5");
    }
    
    /**
     * Create an RC4 key
     *
     * @param keyBytes the bytes for the key
     * @return the key
     */
    private SymmetricKey createRC4Key(byte[] keyBytes)
    {
    	return new ARC4Key(keyBytes);
    }
    
    /**
     * Hash into an existing byte array
     * @param md5 the MD5 digest
     * @param hash the hash destination
     * @throws CryptoException if there's a problem hashing; e.g.,
     *  if the buffer is too small
     */
    private void digestTo(Digest md5, byte[] hash) throws CryptoException
    {
        md5.getDigest(hash, 0);
    }
}
